package net.ib.mtalk.network.pipeline;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;
import java.util.Map;

import net.ib.mntalk.protocol.Common.Envelope;
import net.ib.mtalk.network.MethodId;
import net.ib.mtalk.network.TcpEnvelope;
import net.ib.mtalk.util.MTalkLogWriter;

import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;

import android.os.PowerManager;
import android.os.SystemClock;

/**
 * TcpEnvelope <-> Envelope<br>
 * 송수신 매칭과 콜백의 저장도 이 수준에서 이루어진다.
 * @author Arngard
 */
public class TcpEnvelopeBicoder extends OneToOneBicoder {
	
	private PowerManager.WakeLock mWakeLock;
	/** 보낸 리퀘스트를 기록 */
	protected Map<Integer, TcpEnvelope> sendHistory = new Hashtable<Integer, TcpEnvelope>();
	public static Object historyLock = new Object();

	public TcpEnvelopeBicoder(PowerManager.WakeLock wakeLock) {
		this.mWakeLock = wakeLock;
	}

	protected Object encode(ChannelHandlerContext ctx, Channel channel, Object object) throws Exception {
		MTalkLogWriter.v(this.getClass().getSimpleName()+".encode() - STT");

		if(mWakeLock != null) mWakeLock.acquire();
		try {
			TcpEnvelope revelop = (TcpEnvelope) object;

			if (revelop.getType() == TcpEnvelope.REQUEST) {
				addHistory(revelop);	// 보내는 질문은 기억해둔다. 응답이면 기억하지 않음.
			}

			return revelop.toEnvelope();
		} finally {
			if(mWakeLock != null) mWakeLock.release();
			
			MTalkLogWriter.v(this.getClass().getSimpleName()+".encode() - END");
		}
	}
	

	protected Object decode(ChannelHandlerContext ctx, Channel channel, Object object) throws Exception {
		MTalkLogWriter.v(this.getClass().getSimpleName()+".decode() - STT");

		if(mWakeLock != null) mWakeLock.acquire();
		try {
			Envelope received = (Envelope) object;
			if (received == null) {
				MTalkLogWriter.e(this.getClass().getSimpleName()+".decode() : Should not reach here.");
			}
			
			switch (received.getType()) {	// envelope이 질문인지 응답인지에 따른 분기
			case TcpEnvelope.REQUEST:
				return decodeRequest(received);
			case TcpEnvelope.RESPONSE:
				return decodeResponse(ctx, received);
			default:
				MTalkLogWriter.e(this.getClass().getSimpleName()+".decode() : Should not reach here.");
				return null;
			}
		} finally {
			if(mWakeLock != null) mWakeLock.release();
			
			MTalkLogWriter.v(this.getClass().getSimpleName()+".decode() - END");
		}
	}

	/**
	 * 서버의 응답이 온 경우.<br>
	 * Envelope 응답 객체를 Revelope으로 재해석한다.
	 * @param received 받은 응답
	 */
	protected TcpEnvelope decodeResponse(ChannelHandlerContext ctx, Envelope received) {
		// 이 응답은 이전에 보냈던 나의 질문에 대한 응답일 것이다.
		TcpEnvelope sentRevelop = extractReqFromHistory(received);
		if (sentRevelop == null) {	// 대응되는 적절한 질문이 없다. 뭘 보낸 거냐 서버...?
			// 서버에 응답을 할 필요는 없다.
			// 이 응답이 무엇에 대한 응답인지 모르기 때문에 처리도 할 수 없다.
			MTalkLogWriter.w(this.getClass().getSimpleName()+".decodeResponse(): Cannot Find Request for this Response.");

			//TcpEnvelope receivedRevelope = new TcpEnvelope.Builder()
			//.setRequestId(received.getRequestId())
			//.setType(TcpEnvelope.RESPONSE)
			//.setMethodId(received.getMethodId())
			//.setMessage(MethodId.toResponseMessage(received.getMethodId(), received.getBody().toByteArray()))
			//.build();

			return null;
		}
		
		// 이전의 질문 기록을 찾았다면 이를 참고하여 응답 객체를 구성
		TcpEnvelope receivedRevelope = new TcpEnvelope.Builder()
		.setRequestId(received.getRequestId())
		.setType(TcpEnvelope.RESPONSE)
		.setMethodId(received.getMethodId())
		.setMessage(MethodId.toResponseMessage(received.getMethodId(), received.getBody().toByteArray()))
		.setCallback(sentRevelop.getCallback())
		.build();

		// 콜백의 정상 호출을 수행한다.
		MTalkLogWriter.v(this.getClass().getSimpleName()+".decodeResponse() : tcp normal callback");
		receivedRevelope.passResult(sentRevelop, receivedRevelope);

		return receivedRevelope;
	}
	
	/**
	 * 서버가 질문을 한 경우.<br>
	 * Envelope 응답 객체를 Revelope으로 재해석한다.
	 * @param received 받은 질문
	 */
	protected TcpEnvelope decodeRequest(Envelope received) {
		// 내용을 옮겨 채우고
		TcpEnvelope.Builder receivedRevelopeBuilder = new TcpEnvelope.Builder()
		.setRequestId(received.getRequestId())
		.setType(TcpEnvelope.REQUEST)
		.setMethodId(received.getMethodId())
		.setMessage(MethodId.toRequestMessage(received.getMethodId(), received.getBody().toByteArray()));

		// MethodId에 따라 적당한 콜백 객체를 배정해준다.
		switch (received.getMethodId()) {
		case MethodId.Client.Messaging.DeliverMessages:
			receivedRevelopeBuilder.setCallback(net.ib.mtalk.network.clientapi.DeliverMessages.getCallback());
			break;
		case MethodId.Client.Messaging.DeliverReadMarks:
			receivedRevelopeBuilder.setCallback(net.ib.mtalk.network.clientapi.DeliverReadMarks.getCallback());
			break;
		case MethodId.Client.Ping.Ping:
			receivedRevelopeBuilder.setCallback(net.ib.mtalk.network.clientapi.Ping.getCallback());
			break;
		default:
			// MethodId를 해석할 수 없다면 응답 객체의 타입을 지정할 수 없기 때문에 어차피 응답할 수 없다.
			MTalkLogWriter.e(this.getClass().getSimpleName()+".decodeRequest() : Should not reach here.");
			break;
		}
		
		TcpEnvelope receivedRevelope = receivedRevelopeBuilder.build();
		
		MTalkLogWriter.v(this.getClass().getSimpleName()+".decodeRequest() : tcp normal callback");
		receivedRevelope.passResult(null, receivedRevelope);
		
		return receivedRevelope;
	}
	
	/**
	 * 이 Envelope 응답을 유도한 TcpEnvelope 질문이 무엇인지, 이전에 보냈던 TcpEnvelope 중에서 찾아본다.
	 * @param received 받은 Envelope
	 * @return 질문을 찾아내면 해당 질문 객체를 리턴. 못 찾으면 null.
	 */
	public TcpEnvelope extractReqFromHistory(Envelope received) {
		synchronized (historyLock) {
			int recvRequestId = received.getRequestId();
			int recvMethodId = received.getMethodId();
			
			MTalkLogWriter.v("TcpEnvelopeBicoder.extractReqFromHistory() : RequestId="+recvRequestId+", MethodId="+recvMethodId);
			
			TcpEnvelope sentItem = sendHistory.remove(recvRequestId);
			if (sentItem == null) {
				MTalkLogWriter.w("TcpEnvelopeBicoder.extractReqFromHistory() : sentItem == null");
				return null;
			}
			if (sentItem.getRequestId() != recvRequestId) {	// RequestId 다르면 걸러내기
				MTalkLogWriter.w("TcpEnvelopeBicoder.extractReqFromHistory() : different RequestId");
				return null;
			}
			if (sentItem.getMethodId() != recvMethodId) {	// MethodId 다르면 걸러내기
				MTalkLogWriter.w("TcpEnvelopeBicoder.extractReqFromHistory() : different MethodId");
				return null;
			}
			if (sentItem.hasExpired()) {	// 유통기한 지났으면 찾았어도 무효. 에러 콜백이나 해 주자.
				MTalkLogWriter.w("TcpEnvelopeBicoder.extractReqFromHistory() : expired. try tcp error callback.");
				sentItem.passError(sentItem);
				return null;
			}
			if (sentItem.getType() != TcpEnvelope.REQUEST) {	// 질문이 아니었다?
				MTalkLogWriter.e("TcpEnvelopeBicoder.extractReqFromHistory(): Should not reach here.");
				return null;
			}
			return sentItem;
		}
	}

	/**
	 * 전송 기록을 추가한다.
	 * @param toSend 추가할 것
	 */
	public void addHistory(TcpEnvelope toSend) {
		synchronized (historyLock) {
			MTalkLogWriter.v("TcpEnvelopeBicoder.addHistory() : RequestId="+toSend.getRequestId()+", MethodId="+toSend.getMethodId());
	
			sendHistory.put(toSend.getRequestId(), toSend);
		}
	}

	/**
	 * 기록에 있던 모든 요청에 대해 에러 콜백을 하고 기록에서 삭제한다.
	 * */
	public void kickHistory() {
		synchronized (historyLock) {
			for (TcpEnvelope item : sendHistory.values()) {
				MTalkLogWriter.v("TcpEnvelopeBicoder.kickHistory() : try tcp error callback");
				item.passError(item);
			}
			sendHistory.clear();
		}
	}

	/**
	 * 전송 기록에 남아있는 것들 중에서 유통기한이 지난 것들에 대해 에러 콜백을 해준다.
	 */
	public void expiredCollector() {
		synchronized (historyLock) {
			MTalkLogWriter.v("TcpEnvelopeBicoder.expiredCollector() - STT");
			
			long currentTime = SystemClock.elapsedRealtime();	// 현재 시각
			int count = 0;
		
			ArrayList<TcpEnvelope> snapshot = new ArrayList<TcpEnvelope>(sendHistory.values());	// 전송기록의 스냅샷을 추출
			if (snapshot != null) {
				for (TcpEnvelope sentItem : snapshot) {
					if (sentItem == null)
						continue;
					if (! sentItem.hasExpired(currentTime))	// 유통기한이 지나지 않았으면 패스
						continue;
					
					++count;
					sendHistory.remove(sentItem.getRequestId());	// 기록에서 제거하고
					MTalkLogWriter.v("TcpEnvelopeBicoder.expiredCollector() : try tcp error callback");
					sentItem.passError(sentItem);	// 에러 콜백
				}
			}
			if (count > 0) {
				MTalkLogWriter.d("TcpEnvelopeBicoder.expiredCollector() : collected "+count+" expired TcpEnvelope(s).");
			}
			
			MTalkLogWriter.v("TcpEnvelopeBicoder.expiredCollector() - END");
		}
	}
	
}
